﻿using System;
using System.Collections.Generic;
using System.Data.Linq;

using RioTinto.Data.Interfaces;

namespace RioTinto.Data
{
    /// <summary>
    /// Manages data context within the scope of the current thread.
    /// </summary>
    /// <remarks>
    /// The purpose of DataContextProvider is to allow control over
    /// the scope of DataContext to allow the use of Transactions
    /// as well as the ability to use Sytem.Data.Linq.DataLoadOptions
    /// to preload related data and improve performance by reducing
    /// database roundtrips.
    /// </remarks>
    public class DataContextProvider
    {
        /// <summary>
        /// Default implementation of DataContextFactory uses data.dbml 
        /// defined locally in current project.
        /// </summary>
        private class DefaultDataContextFactory : IDataContextFactory
        {
            #region IDataContextFactory Members

            /// <summary>
            /// Gets a new DataContext with the configured connection string
            /// </summary>
            public DataContext CreateDataContext()
            {
                return new RTDataContext(RioTinto.Helpers.EnvironmentState.CurrentConnectionString);
            }

            #endregion
        }

        /// <summary>
        /// Gets a reference to the current IDataContextFactory
        /// used to get a DataContext object within the 
        /// current scope of the current thread.
        /// </summary>
        private static IDataContextFactory m_Factory;
        private static IDataContextFactory Factory
        {
            get
            {
                //make sure factory isn't null
                if (m_Factory == null)
                    m_Factory = new DefaultDataContextFactory();

                return m_Factory;
            }
        }

        /// <summary>
        /// Sets the DataContextFactory for the application
        /// </summary>
        /// <remarks>
        /// Allows the application to define its own DataContext
        /// to be used globally for all data access.
        /// </remarks>
        /// <param name="factory"></param>
        public static void SetDataContextFactory(IDataContextFactory factory)
        {
            m_Factory = factory;
        }

        /// <summary>
        /// Thread lock mechanism to make operations thread-safe
        /// </summary>
        private static Object m_SyncLock = new Object();

        /// <summary>
        /// Manages the scope of the current DataContext
        /// </summary>
        private static Stack<String> ContextKeys
        {
            get
            {
                // if there is no ThreadLocalStorageProvider then it won't be managed. 
                // just return an empty stack
                if (ThreadLocalStorageProvider.Current == null)
                    return new Stack<String>(new String[] { "DataContext" });

                if (!ThreadLocalStorageProvider.Current.ContainsItem("ContextKeys"))
                {
                    // use a lock so we can create a new stack in a thread-safe manner
                    lock (m_SyncLock)
                    {
                        // now we have a lock, make sure another thread didn't already create
                        // a new stack while we were waiting for the lock
                        if (!ThreadLocalStorageProvider.Current.ContainsItem("ContextKeys"))
                        {
                            // set the root context key
                            Stack<String> keys = new Stack<String>();
                            keys.Push("DataContext");

                            // add the root context key (this operation is thread-safe due to our lock)
                            ThreadLocalStorageProvider.Current.SaveItem("ContextKeys", keys);
                        }
                    }
                }

                return (Stack<String>)ThreadLocalStorageProvider.Current.GetItem("ContextKeys");
            }
        }

        /// <summary>
        /// The the current, default scoped DataContext
        /// </summary>
        /// <returns></returns>
        public static DataContext GetDataContext()
        {
            return GetDataContext(ContextKeys.Peek());
        }

        /// <summary>
        /// The the current named DataContext
        /// </summary>
        /// <param name="key">user defined key to identify a scope outside the default scope</param>
        /// <returns>Current DataContext w/in the user-defined scope</returns>
        public static DataContext GetDataContext(String key)
        {
            // if TLS isn't available just return a new DataContext
            if (ThreadLocalStorageProvider.Current == null)
                return Factory.CreateDataContext();

            DataContext context = null;

            // check to see if DataContext already exists in TLS
            if (ThreadLocalStorageProvider.Current.ContainsItem(key))
            {
                // get DataContext
                context = ThreadLocalStorageProvider.Current.GetItem(key) as DataContext;

                // verify DataContext
                if (context != null)
                    return context;
            }

            // if we got here, then just create a new DataContext
            context = CreateDataContext(key, false);

            // return to caller
            return context;
        }

        /// <summary>
        /// Creates a new DataContext within the scope of the specified key
        /// </summary>
        /// <param name="key">a key to define the scope of the DataContext</param>
        /// <returns>Current DataContext within the specified scope</returns>
        public static DataContext CreateDataContext(String key)
        {
            return CreateDataContext(key, false);
        }

        /// <summary>
        /// Creates a new DataContext within the scope of the specified key
        /// </summary>
        /// <param name="key">a key to define the scope of the DataContext</param>
        /// <param name="setAsCurrentScope">Indicates if DataContext should be registered 
        /// as the global default for the current thread.</param>
        /// <remarks>If setAsCurrentScope is true and a default DataContext already
        /// exists for the current scope, then the new DataContext will be a child
        /// of the existing default.</remarks>
        /// <returns>Current DataContext within the specified scope</returns>
        internal static DataContext CreateDataContext(String key, Boolean setAsCurrentScope)
        {
            if (ThreadLocalStorageProvider.Current.ContainsItem(key))
                throw new InvalidOperationException(string.Format("DataContext with the name '{0}' already exists", key));

            DataContext context = Factory.CreateDataContext();

            ThreadLocalStorageProvider.Current.SaveItem(key, context);

            if (setAsCurrentScope)
            {
                // push current key to the top of the stack
                ContextKeys.Push(key);
            }

            return context;
        }

        /// <summary>
        /// Removes the specified DataContext
        /// </summary>
        /// <param name="key"></param>
        public static void RemoveDataContext(String key)
        {
            RemoveDataContext(key, false);
        }

        /// <summary>
        /// Removes the specified DataContext
        /// </summary>
        /// <param name="key"></param>
        public static void RemoveDataContext(String key, Boolean removeFromCurrentScope)
        {
            if (!ThreadLocalStorageProvider.Current.ContainsItem(key))
                throw new IndexOutOfRangeException(String.Format("DataContext with the name '{0}' does not exist", key));

            DataContext context = (DataContext)ThreadLocalStorageProvider.Current.GetItem(key);

            ThreadLocalStorageProvider.Current.RemoveItem(key);

            if (context.Connection.State == System.Data.ConnectionState.Open)
                context.Dispose();

            if (removeFromCurrentScope)
                ContextKeys.Pop();
        }

    }
}
